package com.smart.tool.retry;

import java.util.concurrent.TimeUnit;

import com.smart.tool.retry.core.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @Author gao.gwq
 * @Version LocalRetryer.java, v 0.1 2024年02月26日 10:40 gao.gwq
 * @Description: 编程式内存重试的通用类，当抛出异常时，会进行重试
 *
 * example 1:
 *String result = LocalRetryer.of(3, 1000, 1000).retry(new Retry<String>() {
     @Override
     public String execute() throws Throwable {
     return "hello";
     }
  });
 *example 2:
 * LocalRetryer.of(3, 1000, 1000).retryWtihoutResult(new RetryWithoutResult() {
     @Override
     public void execute() throws Throwable {
        int a = 0;
        System.out.println(1/a);
     }
  });
 *
 */
public class LocalRetryer {

    private static final Logger log = LoggerFactory.getLogger(LocalRetryer.class);

    private Retry retry;

    private int maxAttempTimes;

    private int intervalTime;

    private int firstDelayTime;

    /**
     * 默认是固定间隔策略
     */
    private WaitStrategyEnum waitStrategyEnum = WaitStrategyEnum.FIXED_WAIT;


    private TimeUnit waitType = TimeUnit.SECONDS;

    private LocalRetryer(int maxAttempTimes,
                         int intervalTime,
                         int firstDelayTime) {
        this.maxAttempTimes = maxAttempTimes;
        this.intervalTime = intervalTime;
        this.firstDelayTime = firstDelayTime;
    }

    private LocalRetryer(int maxAttempTimes, int intervalTime,
                         int firstDelayTime,TimeUnit waitType,WaitStrategyEnum waitStrategy) {
        this.maxAttempTimes = maxAttempTimes;
        this.intervalTime = intervalTime;
        this.firstDelayTime = firstDelayTime;
        this.waitType = waitType;
        this.waitStrategyEnum = waitStrategy;
    }
    public <R> Retry<R> getRetryFunction() {
        return retry;
    }

    public <R> LocalRetryer setRetryFunction(Retry<R> retry) {
        this.retry = retry;
        return this;
    }

    public int getMaxAttempTimes() {
        return maxAttempTimes;
    }

    public LocalRetryer setMaxAttempTimes(int maxAttempTimes) {
        this.maxAttempTimes = maxAttempTimes;
        return this;
    }

    public int getIntervalTime() {
        return intervalTime;
    }

    public LocalRetryer setIntervalTime(int intervalTime) {
        this.intervalTime = intervalTime;
        return this;
    }

    public int getFirstDelayTime() {
        return firstDelayTime;
    }

    public LocalRetryer setFirstDelayTime(int firstDelayTime) {
        this.firstDelayTime = firstDelayTime;
        return this;
    }

    public TimeUnit getWaitType() {
        return waitType;
    }

    public LocalRetryer setWaitType(TimeUnit waitType) {
        this.waitType = waitType;
        return this;
    }




    public static  LocalRetryer of(int maxAttempTimes,
                                   int intervalTime,
                                   int firstDelayTime,
                                   TimeUnit waitType,WaitStrategyEnum waitStrategy){
        if(maxAttempTimes <= 0){
            throw new IllegalArgumentException("maxAttempTimes must be greater than 0");
        }
        if(intervalTime <= 0) {
            throw new IllegalArgumentException("intervalTime must be greater than 0");
        }
        if(waitType == null){
            throw new IllegalArgumentException("waitType cannot be null");
        }
        if(waitStrategy == null){
            throw new IllegalArgumentException("waitStrategy cannot be null");
        }
        return new LocalRetryer( maxAttempTimes, intervalTime, firstDelayTime,waitType, waitStrategy);
    }

    public static  LocalRetryer of(int maxAttempTimes, int intervalTime, int firstDelayTime){
        if(maxAttempTimes <= 0){
            throw new IllegalArgumentException("maxAttempTimes must be greater than 0");
        }
        if(intervalTime <= 0){
            throw new IllegalArgumentException("intervalTime must be greater than 0");
        }
        return  LocalRetryer.of( maxAttempTimes,
                intervalTime,
                firstDelayTime,
                TimeUnit.SECONDS,WaitStrategyEnum.FIXED_WAIT);
    }


    public void retryWtihoutResult(RetryWithoutResult retryFunction) throws Throwable {
        this.retry = retryFunction;
        retry(retryFunction);
    }

    public <R> R retry(Retry<R> retry)  {
        this.retry = retry;
        return retry();
    }



    private <R> R retry() {


        StopStrategy stopStrategy = StopStrategies.stopAfterAttempt(maxAttempTimes);




        BlockStrategy blockStrategy = BlockStrategies.threadSleepStrategy();

        if (firstDelayTime > 0) {
            try {
                blockStrategy.block(firstDelayTime);
            } catch (InterruptedException ex) {

                Thread.currentThread().interrupt();
                log.warn("[LocalRetryer-retry] firstDelay block error ", ex);
                throw new RuntimeException(ex);
            }
        }

        Throwable lastException = null;
        R lastResult = null;
        int retryCount = 0;
        try {
            long startTime = System.nanoTime();

            for (int attemptNumber = 1; ; attemptNumber++) {

                Attempt<R> attempt;
                Throwable tmpThrowable = null;
                R tmpResult = null;
                try {
                    R result = (R)retry.execute();
                    tmpResult = result;
                    lastResult = result;
                    retryCount = attemptNumber;
                    //if(methodInvocation instanceof ProxyMethodInvocation)
                    attempt = new ResultAttempt<R>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
                } catch (Throwable throwable) {
                    tmpThrowable = throwable;
                    lastException = throwable;
                    attempt = new ExceptionAttempt<R>(throwable, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
                }finally {
                    try {
                        retry.completeOneRetry(attemptNumber,tmpResult,tmpThrowable);
                    }catch (Throwable throwable){
                        log.error("[LocalRetryer-retry] the {} retry completeOneRetry error ", attemptNumber,throwable);
                    }

                }
                if (!judgeRetry(attempt)) {
                    return attempt.get();
                }

                if (stopStrategy.shouldStop(attempt)) {
                    return attempt.get();
                }
                WaitStrategy waitStrategy = WaitStrategies.routeWaitStrategy(intervalTime, waitType, waitStrategyEnum);

                long sleepTime = waitStrategy.computeSleepTime(attempt,attemptNumber);
                try {
                    blockStrategy.block(sleepTime);
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();
                    log.warn("[LocalRetryer-retry] the {} retry block error ", attemptNumber, ex);
                    throw new RuntimeException(ex);
                }
            }
        }finally {
            try {
                retry.completeAllRetry(retryCount,lastResult,lastException);
            }catch (Throwable throwable){
                log.warn("[LocalRetryer-retry] the {} retry completeAllRetry error ", retryCount, throwable);
            }

        }


        // return null;
    }

    /**
     * 内存重试，判断能否进行重试
     * 1、如果抛出异常，则认为需要重试。无论此次异常是否在注解的范围内
     * 2、如果是基于结果实现的，如果结果不是${link TaskStatus.SUCESS}
     *
     * @param attempt
     * @return
     */
    private boolean judgeRetry(Attempt attempt) {
        if (attempt.hasException()) {
            return true;
        }
        return false;
    }

    static final class ExceptionAttempt<R> implements Attempt<R> {
        private final Throwable e;
        private final long attemptNumber;
        private final long delaySinceFirstAttempt;

        public ExceptionAttempt(Throwable cause, long attemptNumber, long delaySinceFirstAttempt) {
            this.e = cause;
            this.attemptNumber = attemptNumber;
            this.delaySinceFirstAttempt = delaySinceFirstAttempt;
        }

        @Override
        public R get() {
            throw new RetryException(e);
        }

        @Override
        public boolean hasResult() {
            return false;
        }

        @Override
        public boolean hasException() {
            return true;
        }

        @Override
        public R getResult() throws IllegalStateException {
            throw new IllegalStateException("The attempt resulted in an exception, not in a result");
        }

        @Override
        public Throwable getExceptionCause() throws IllegalStateException {
            return e.getCause();
        }

        @Override
        public long getAttemptNumber() {
            return attemptNumber;
        }

        @Override
        public long getDelaySinceFirstAttempt() {
            return delaySinceFirstAttempt;
        }
    }

    static final class ResultAttempt<R> implements Attempt<R> {
        private final R result;
        private final long attemptNumber;
        private final long delaySinceFirstAttempt;

        public ResultAttempt(R result, long attemptNumber, long delaySinceFirstAttempt) {
            this.result = result;
            this.attemptNumber = attemptNumber;
            this.delaySinceFirstAttempt = delaySinceFirstAttempt;
        }

        @Override
        public R get() {
            return result;
        }

        @Override
        public boolean hasResult() {
            return true;
        }

        @Override
        public boolean hasException() {
            return false;
        }

        @Override
        public R getResult() throws IllegalStateException {
            return result;
        }

        @Override
        public Throwable getExceptionCause() throws IllegalStateException {
            throw new IllegalStateException("The attempt resulted in a result, not in an exception");
        }

        @Override
        public long getAttemptNumber() {
            return attemptNumber;
        }

        @Override
        public long getDelaySinceFirstAttempt() {
            return delaySinceFirstAttempt;
        }
    }
}
